spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import type { SourceRef } from "@dci/core";2import type { Metadata } from "next";3import Link from "next/link";4import { notFound } from "next/navigation";5import { Breadcrumbs } from "@/components/entity/breadcrumbs";6import { JsonLd } from "@/components/entity/json-ld";7import { ExtLink, KeyValueList } from "@/components/entity/kv";8import { SectionNav } from "@/components/entity/section-nav";9import { SourcesFooter } from "@/components/entity/sources-footer";10import { CoverageBars } from "./coverage-bars";11import { Badge } from "@/components/ui/badge";12import { Button } from "@/components/ui/button";13import { Page, Section } from "@/components/ui/card";14import { Metric, MetricCell, MetricRow } from "@/components/ui/metric";15import { ErrorState, OfflineNotice } from "@/components/ui/states";16import { api } from "@/lib/api";17import { fmtDate, fmtInt } from "@/lib/format";18import { ENTITY_TYPE_LABEL, SOURCE_KIND_LABEL } from "@/lib/labels";19import { routes } from "@/lib/routes";20import { NOT_FOUND_METADATA, absoluteUrl, compactJsonLd, pageMetadata } from "@/lib/seo";2122export const revalidate = 300;2324type Params = Promise<{ id: string }>;2526export async function generateMetadata({ params }: { params: Params }): Promise<Metadata> {27 const { id } = await params;28 const r = await api.source(id);29 if (!r.ok) return r.status === 404 ? NOT_FOUND_METADATA : { title: "Source" };30 const s = r.data;31 return pageMetadata({32 title: s.name,33 description: `${s.name} (${SOURCE_KIND_LABEL[s.kind].toLowerCase()} source, ${s.domain}) — ${fmtInt(s.documents)} documents, ${fmtInt(s.provenanceRows)} provenance rows across ${fmtInt(s.facilities)} facilities in DataCenterIndex.`,34 path: routes.source(s.id),35 });36}3738function humanise(s: string): string {39 return s.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());40}4142export default async function SourcePage({ params }: { params: Params }) {43 const { id } = await params;44 const r = await api.source(id);45 if (!r.ok && r.status === 404) notFound();46 if (!r.ok) {47 return (48 <Page className="py-6">49 <Breadcrumbs items={[{ name: "Sources", href: routes.sources() }, { name: id, href: routes.source(id) }]} />50 <OfflineNotice detail={r.error} className="my-4" />51 <ErrorState />52 </Page>53 );54 }55 const s = r.data;56 const ref: SourceRef = { id: s.id, name: s.name, domain: s.domain, kind: s.kind, url: s.url, license: s.license, attribution: s.attribution };57 const robots = s.robotsAllowed === true ? { tone: "ok" as const, label: "robots.txt allows" } : s.robotsAllowed === false ? { tone: "warn" as const, label: "robots.txt disallows (API endpoint — see notes)" } : { tone: "neutral" as const, label: "robots.txt not evaluated" };58 const entityRows = s.byEntityType.map((e) => ({ label: ENTITY_TYPE_LABEL[e.entityType] ?? humanise(e.entityType), value: e.count }));59 const pageRows = s.byPageType.map((p) => ({ label: humanise(p.pageType), value: p.count }));60 const nav = [61 { id: "licence", label: "Licence" },62 { id: "notes", label: "Notes", count: s.notes ? 1 : 0 },63 { id: "coverage", label: "Coverage", count: entityRows.length + pageRows.length },64 { id: "connector", label: "Connector" },65 { id: "facilities", label: "Facilities" },66 ];6768 return (69 <Page>70 <Breadcrumbs items={[{ name: "Sources", href: routes.sources() }, { name: s.name, href: routes.source(s.id) }]} className="pt-3" />71 <JsonLd data={compactJsonLd({ "@context": "https://schema.org", "@type": "Organization", "@id": `${absoluteUrl(routes.source(s.id))}#org`, name: s.name, url: s.url ?? `https://${s.domain}`, description: s.attribution })} />7273 <header className="flex flex-col gap-2 py-5 sm:py-7">74 <div className="label-caps">Source</div>75 <h1 className="text-[22px] font-semibold leading-tight tracking-[-0.02em] text-ink sm:text-[28px]">{s.name}</h1>76 <div className="flex flex-wrap items-center gap-2 text-[13px] text-ink-2">77 <Badge tone="outline">{SOURCE_KIND_LABEL[s.kind]}</Badge>78 <Badge tone={robots.tone} size="sm">79 {robots.label}80 </Badge>81 <span className="text-ink-4" aria-hidden>82 ·83 </span>84 <ExtLink href={`https://${s.domain}`}>{s.domain}</ExtLink>85 {s.url && (86 <>87 <span className="text-ink-4" aria-hidden>88 ·89 </span>90 <ExtLink href={s.url}>Licence / terms</ExtLink>91 </>92 )}93 <span className="figure text-[11.5px] text-ink-4" title="Authority rank used when values conflict (lower = higher authority)">94 priority {s.priority}95 </span>96 </div>97 </header>9899 <MetricRow cols={5}>100 <MetricCell>101 <Metric value={fmtInt(s.documents)} label="Documents" size="sm" />102 </MetricCell>103 <MetricCell>104 <Metric value={fmtInt(s.provenanceRows)} label="Provenance rows" size="sm" />105 </MetricCell>106 <MetricCell>107 <Metric value={fmtInt(s.facilities)} label="Facilities backed" size="sm" />108 </MetricCell>109 <MetricCell>110 <Metric value={fmtDate(s.lastDocumentAt)} label="Last document" size="sm" />111 </MetricCell>112 <MetricCell>113 <Metric value={fmtDate(s.updatedAt)} label="Registry updated" size="sm" />114 </MetricCell>115 </MetricRow>116117 <SectionNav items={nav} className="mt-4" />118119 <div className="mt-6 flex flex-col gap-8">120 <Section id="licence" className="scroll-mt-28" title="Licence & attribution" description="Recorded per source; the attribution line is shown on every page that uses a value from this source.">121 <KeyValueList122 items={[123 { label: "Licence", value: s.license ?? <span className="text-ink-3">Not recorded — facts only are extracted; the source’s own terms apply.</span>, keepEmpty: true },124 { label: "Attribution", value: s.attribution ?? <span className="text-ink-3">{`Source: ${s.name}`}</span>, keepEmpty: true },125 { label: "Terms URL", value: s.url ? <ExtLink href={s.url} /> : null },126 ]}127 />128 </Section>129130 {s.notes && (131 <Section id="notes" className="scroll-mt-28" title="Notes" description="Quirks recorded by the connector author (access method, limits, robots exceptions).">132 <p className="max-w-3xl whitespace-pre-line text-[13.5px] leading-relaxed text-ink-2">{s.notes}</p>133 </Section>134 )}135136 {(entityRows.length > 0 || pageRows.length > 0) && (137 <Section id="coverage" className="scroll-mt-28" title="Coverage" description="Current provenance rows by entity type, and archived documents by page type.">138 <div className="grid gap-6 md:grid-cols-2">139 {entityRows.length > 0 && (140 <div>141 <div className="label-caps mb-2">Provenance rows by entity</div>142 <CoverageBars items={entityRows} />143 </div>144 )}145 {pageRows.length > 0 && (146 <div>147 <div className="label-caps mb-2">Documents by page type</div>148 <CoverageBars items={pageRows} />149 </div>150 )}151 </div>152 </Section>153 )}154155 <Section id="connector" className="scroll-mt-28" title="Connector" description="How this source is read.">156 <KeyValueList157 items={[158 { label: "Connector id", value: <span className="figure">{s.connectorId}</span> },159 { label: "Kind", value: SOURCE_KIND_LABEL[s.kind] },160 { label: "robots.txt", value: robots.label },161 {162 label: "Pipeline",163 value: (164 <Link href={`${routes.methodology()}#pipeline`} className="underline decoration-hair-2 hover:text-accent">165 discover → fetch → archive → extract → normalize → reconcile → publish166 </Link>167 ),168 },169 ]}170 />171 </Section>172173 <Section id="facilities" className="scroll-mt-28" title="Facilities backed by this source" description={`${fmtInt(s.facilities)} facilities carry at least one current value observed here.`}>174 <p className="max-w-2xl text-[13px] text-ink-2">The public index does not filter facilities by source yet. Browse the facilities index — each facility page lists, field by field, which source backs which value, with first/last observation dates and a link to the page it was read from.</p>175 <div className="mt-3">176 <Button href={routes.datacenters()} size="sm">177 Browse facilities178 </Button>179 </div>180 </Section>181 </div>182183 <SourcesFooter sources={[ref]} updatedAt={s.updatedAt} note="Licence and attribution are recorded per source and shown wherever its values appear." />184 </Page>185 );186}187